/* SNMP support
 * Copyright (C) 2012 Vincent Bernat <bernat@luffy.cx>
 *
 * This file is part of GNU Zebra.
 *
 * GNU Zebra is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * GNU Zebra is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GNU Zebra; see the file COPYING.  If not, write to the Free
 * Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.  
 */

#include <zebra.h>

#if defined HAVE_SNMP && defined SNMP_AGENTX
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>
#include <net-snmp/agent/snmp_vars.h>

#include "command.h"
#include "smux.h"

static int agentx_enabled = 0;

static struct thread_master *agentx_tm;
static struct thread *timeout_thr = NULL;
static struct list *events = NULL;

static void agentx_events_update(void);

static int
agentx_timeout(struct thread *t)
{
  timeout_thr = NULL;

  snmp_timeout ();
  run_alarms ();
  netsnmp_check_outstanding_agent_requests ();
  agentx_events_update ();
  return 0;
}

static int
agentx_read(struct thread *t)
{
  fd_set fds;
  struct listnode *ln = THREAD_ARG (t);
  list_delete_node (events, ln);

  FD_ZERO (&fds);
  FD_SET (THREAD_FD (t), &fds);
  snmp_read (&fds);

  netsnmp_check_outstanding_agent_requests ();
  agentx_events_update ();
  return 0;
}

static void
agentx_events_update(void)
{
  int maxfd = 0;
  int block = 1;
  struct timeval timeout = { .tv_sec = 0, .tv_usec = 0 };
  fd_set fds;
  struct listnode *ln;
  struct thread *thr;
  int fd, thr_fd;

  THREAD_OFF (timeout_thr);

  FD_ZERO (&fds);
  snmp_select_info (&maxfd, &fds, &timeout, &block);

  if (!block)
    timeout_thr = thread_add_timer_tv (agentx_tm, agentx_timeout, NULL, &timeout);

  ln = listhead (events);
  thr = ln ? listgetdata (ln) : NULL;
  thr_fd = thr ? THREAD_FD (thr) : -1;

  /* "two-pointer" / two-list simultaneous iteration
   * ln/thr/thr_fd point to the next existing event listener to hit while
   * fd counts to catch up */
  for (fd = 0; fd < maxfd; fd++)
    {
      /* caught up */
      if (thr_fd == fd)
        {
          struct listnode *nextln = listnextnode (ln);
          if (!FD_ISSET (fd, &fds))
            {
              thread_cancel (thr);
              list_delete_node (events, ln);
            }
          ln = nextln;
          thr = ln ? listgetdata (ln) : NULL;
          thr_fd = thr ? THREAD_FD (thr) : -1;
        }
      /* need listener, but haven't hit one where it would be */
      else if (FD_ISSET (fd, &fds))
        {
          struct listnode *newln;
          thr = thread_add_read (agentx_tm, agentx_read, NULL, fd);
          newln = listnode_add_before (events, ln, thr);
          thr->arg = newln;
        }
    }

  /* leftover event listeners at this point have fd > maxfd, delete them */
  while (ln)
    {
      struct listnode *nextln = listnextnode (ln);
      thread_cancel (listgetdata (ln));
      list_delete_node (events, ln);
      ln = nextln;
    }
}

/* AgentX node. */
static struct cmd_node agentx_node =
{
  SMUX_NODE,
  ""                            /* AgentX has no interface. */
};

/* Logging NetSNMP messages */
static int
agentx_log_callback(int major, int minor,
		    void *serverarg, void *clientarg)
{
  struct snmp_log_message *slm = (struct snmp_log_message *)serverarg;
  char *msg = strdup (slm->msg);
  if (msg) msg[strlen(msg)-1] = '\0';
  switch (slm->priority)
    {
    case LOG_EMERG:   zlog_err   ("snmp[emerg]: %s",   msg?msg:slm->msg); break;
    case LOG_ALERT:   zlog_err   ("snmp[alert]: %s",   msg?msg:slm->msg); break;
    case LOG_CRIT:    zlog_err   ("snmp[crit]: %s",    msg?msg:slm->msg); break;
    case LOG_ERR:     zlog_err   ("snmp[err]: %s",     msg?msg:slm->msg); break;
    case LOG_WARNING: zlog_warn  ("snmp[warning]: %s", msg?msg:slm->msg); break;
    case LOG_NOTICE:  zlog_notice("snmp[notice]: %s",  msg?msg:slm->msg); break;
    case LOG_INFO:    zlog_info  ("snmp[info]: %s",    msg?msg:slm->msg); break;
    case LOG_DEBUG:   zlog_debug ("snmp[debug]: %s",   msg?msg:slm->msg); break;
    }
  free(msg);
  return SNMP_ERR_NOERROR;
}

static int
config_write_agentx (struct vty *vty)
{
  if (agentx_enabled)
      vty_out (vty, "agentx%s", VTY_NEWLINE);
  return 0;
}

DEFUN (agentx_enable,
       agentx_enable_cmd,
       "agentx",
       "SNMP AgentX protocol settings\n"
       "SNMP AgentX settings\n")
{
  if (!agentx_enabled)
    {
      init_snmp("quagga");
      events = list_new();
      agentx_events_update ();
      agentx_enabled = 1;
      return CMD_SUCCESS;
    }
  vty_out (vty, "SNMP AgentX already enabled%s", VTY_NEWLINE);
  return CMD_WARNING;
}

DEFUN (no_agentx,
       no_agentx_cmd,
       "no agentx",
       NO_STR
       "SNMP AgentX protocol settings\n"
       "SNMP AgentX settings\n")
{
  if (!agentx_enabled) return CMD_SUCCESS;
  vty_out (vty, "SNMP AgentX support cannot be disabled once enabled%s", VTY_NEWLINE);
  return CMD_WARNING;
}

void
smux_init (struct thread_master *tm)
{
  agentx_tm = tm;

  netsnmp_enable_subagent ();
  snmp_disable_log ();
  snmp_enable_calllog ();
  snmp_register_callback (SNMP_CALLBACK_LIBRARY,
			  SNMP_CALLBACK_LOGGING,
			  agentx_log_callback,
			  NULL);
  init_agent ("quagga");

  install_node (&agentx_node, config_write_agentx);
  install_element (CONFIG_NODE, &agentx_enable_cmd);
  install_element (CONFIG_NODE, &no_agentx_cmd);
}

void
smux_register_mib (const char *descr, struct variable *var, 
		   size_t width, int num,
		   oid name[], size_t namelen)
{
  register_mib (descr, var, width, num, name, namelen);
}

int
smux_trap (struct variable *vp, size_t vp_len,
	   const oid *ename, size_t enamelen,
	   const oid *name, size_t namelen,
	   const oid *iname, size_t inamelen,
	   const struct trap_object *trapobj, size_t trapobjlen,
	   u_char sptrap)
{
  oid objid_snmptrap[] = { 1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0 };
  size_t objid_snmptrap_len = sizeof objid_snmptrap / sizeof (oid);
  oid notification_oid[MAX_OID_LEN];
  size_t notification_oid_len;
  unsigned int i;

  netsnmp_variable_list *notification_vars = NULL;
  if (!agentx_enabled) return 0;

  /* snmpTrapOID */
  oid_copy (notification_oid, ename, enamelen);
  notification_oid[enamelen] = sptrap;
  notification_oid_len = enamelen + 1;
  snmp_varlist_add_variable (&notification_vars,
			     objid_snmptrap, objid_snmptrap_len,
			     ASN_OBJECT_ID,
			     (u_char *) notification_oid,
			     notification_oid_len * sizeof(oid));

  /* Provided bindings */
  for (i = 0; i < trapobjlen; i++)
    {
      unsigned int j;
      oid oid[MAX_OID_LEN];
      size_t oid_len, onamelen;
      u_char *val;
      size_t val_len;
      WriteMethod *wm = NULL;
      struct variable cvp;

      /* Make OID. */
      if (trapobj[i].namelen > 0)
        {
	  /* Columnar object */
	  onamelen = trapobj[i].namelen;
	  oid_copy (oid, name, namelen);
	  oid_copy (oid + namelen, trapobj[i].name, onamelen);
	  oid_copy (oid + namelen + onamelen, iname, inamelen);
	  oid_len = namelen + onamelen + inamelen;
        }
      else
        {
	  /* Scalar object */
	  onamelen = trapobj[i].namelen * (-1);
	  oid_copy (oid, name, namelen);
	  oid_copy (oid + namelen, trapobj[i].name, onamelen);
	  oid[onamelen + namelen] = 0;
	  oid_len = namelen + onamelen + 1;
        }

      /* Locate the appropriate function and type in the MIB registry. */
      for (j = 0; j < vp_len; j++)
	{
	  if (oid_compare (trapobj[i].name, onamelen, vp[j].name, vp[j].namelen) != 0)
	    continue;
	  /* We found the appropriate variable in the MIB registry. */
	  oid_copy(cvp.name, name, namelen);
	  oid_copy(cvp.name + namelen, vp[j].name, vp[j].namelen);
	  cvp.namelen = namelen + vp[j].namelen;
	  cvp.type = vp[j].type;
	  cvp.magic = vp[j].magic;
	  cvp.acl = vp[j].acl;
	  cvp.findVar = vp[j].findVar;
	  /* Grab the result. */
	  val = cvp.findVar (&cvp, oid, &oid_len, 1, &val_len, &wm);
	  if (!val) break;
	  snmp_varlist_add_variable (&notification_vars,
				     oid, oid_len,
				     vp[j].type,
				     val,
				     val_len);
	  break;
	}
    }


  send_v2trap (notification_vars);
  snmp_free_varbind (notification_vars);
  agentx_events_update ();
  return 1;
}

#endif /* HAVE_SNMP */
